/* @flow */

import {sortAlpha} from '../util/misc.js';
import {LOCKFILE_VERSION} from '../constants.js';

function shouldWrapKey(str: string): boolean {
  return str.indexOf('true') === 0 || str.indexOf('false') === 0 ||
         /[:\s\n\\",\[\]]/g.test(str) || /^[0-9]/g.test(str) || !/^[a-zA-Z]/g.test(str);
}

function maybeWrap(str: string | boolean | number): string {
  if (typeof str === 'boolean' || typeof str === 'number' || shouldWrapKey(str)) {
    return JSON.stringify(str);
  } else {
    return str;
  }
}

const priorities: { [key: string]: ?number } = {
  name: 1,
  version: 2,
  uid: 3,
  resolved: 4,
  registry: 5,
  dependencies: 6,
};

function priorityThenAlphaSort(a: string, b: string): number {
  if (priorities[a] || priorities[b]) {
    return (priorities[a] || 100) > (priorities[b] || 100) ? 1 : -1;
  } else {
    return sortAlpha(a, b);
  }
}

type Options = {
  indent: string,
  topLevel?: boolean,
};

function _stringify(obj: { [key: string]: mixed }, options: Options): string {
  if (typeof obj !== 'object') {
    throw new TypeError();
  }

  const indent = options.indent;
  const lines = [];

  // Sorting order needs to be consistent between runs, we run native sort by name because there are no
  // problems with it being unstable because there are no to keys the same
  // However priorities can be duplicated and native sort can shuffle things from run to run
  const keys = Object.keys(obj).sort(priorityThenAlphaSort);

  let addedKeys = [];

  for (let i = 0; i < keys.length; i++) {
    const key = keys[i];
    const val = obj[key];
    if (val == null || addedKeys.indexOf(key) >= 0) {
      continue;
    }

    //
    const valKeys = [key];

    // get all keys that have the same value equality, we only want this for objects
    if (typeof val === 'object') {
      for (let j = i + 1; j < keys.length; j++) {
        const key = keys[j];
        if (val === obj[key]) {
          valKeys.push(key);
        }
      }
    }

    //
    const keyLine = valKeys.sort(sortAlpha).map(maybeWrap).join(', ');

    if (typeof val === 'string' || typeof val === 'boolean' || typeof val === 'number') {
      lines.push(`${keyLine} ${maybeWrap(val)}`);
    } else if (typeof val === 'object') {
      lines.push(
        `${keyLine}:\n${_stringify(val, {indent: indent + '  '})}` +
        (options.topLevel ? '\n' : ''),
      );
    } else {
      throw new TypeError();
    }

    addedKeys = addedKeys.concat(valKeys);
  }

  return indent + lines.join(`\n${indent}`);
}

export default function stringify(obj: Object, noHeader?: boolean): string {
  const val = _stringify(obj, {
    indent: '',
    topLevel: true,
  });
  if (noHeader) {
    return val;
  }

  return [
    '# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.',
    `# yarn lockfile v${LOCKFILE_VERSION}`,
    val,
  ].join('\n');
}
